/***************************************************************************//**
 * @file CGI/CGI_Sun.spp
 * @author  Marek M. Cel <marekcel@mscsim.org>
 *
 * @section LICENSE
 *
 * Copyright (C) 2014 Marek M. Cel
 *
 * This file is part of MScSim. You can redistribute and modify it under
 * the terms of GNU General Public License as published by the Free Software
 * Foundation; either version 3 of the License, or (at your option) any later
 * version.
 *
 * This program is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along
 * with this program; if not, write to the Free Software Foundation, Inc.
 * 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 *
 * Further information about the GNU General Public License can also be found
 * on the world wide web at http://www.gnu.org.
 ******************************************************************************/
#ifndef SKY_DOME_SUN_CPP
#define SKY_DOME_SUN_CPP
#endif

////////////////////////////////////////////////////////////////////////////////

#include <osg/Geode>
#include <osg/Geometry>
#include <osg/Material>
#include <osg/PositionAttitudeTransform>
#include <osg/Texture2D>

#include <osgDB/ReadFile>

#include <Utils.h>

#include <CGI/NodeCallback.h>
#include <CGI/SkyDome.h>
#include <CGI/SkyDomeSun.h>

////////////////////////////////////////////////////////////////////////////////

using namespace std;

////////////////////////////////////////////////////////////////////////////////

const double SkyDomeSun::m_distance = SkyDome::m_radius - 0.005 * SkyDome::m_radius;
const double SkyDomeSun::m_radius   = SkyDomeSun::m_distance * tan( osg::DegreesToRadians(0.27) );

////////////////////////////////////////////////////////////////////////////////

SkyDomeSun::SkyDomeSun( CGI_Data *data ) :
    m_data ( data ),

    m_elevation ( 0.0 ),
    m_azimuth   ( 0.0 )
{
    m_skyDomeSunGroup = createSkyDomeSunGroup();

    m_skyDomeSunData = new DataTypeSkyDomeSun( m_skyDomeSunGroup.get() );

    m_skyDomeSunGroup->setUserData( m_skyDomeSunData.get() );
    m_skyDomeSunGroup->setUpdateCallback( new NodeCallback<DataTypeSkyDomeSun> );
}

////////////////////////////////////////////////////////////////////////////////

void SkyDomeSun::update()
{
    Utils::DateTime dateTime;

    dateTime.year   = m_data->dataInp.dateTime.year;
    dateTime.month  = m_data->dataInp.dateTime.month;
    dateTime.day    = m_data->dataInp.dateTime.day;
    dateTime.hour   = m_data->dataInp.dateTime.hour;
    dateTime.minute = m_data->dataInp.dateTime.minute;
    dateTime.second = m_data->dataInp.dateTime.second;

    double dayNumber = Utils::dayOfYear( dateTime );

    double localHour = m_data->dataInp.dateTime.hour
                     + m_data->dataInp.dateTime.minute / 60.0
                     + m_data->dataInp.dateTime.second / 3600.0
                     + 3.8197186342 * m_data->cameraData.longitude;

    double maxDeclination = osg::DegreesToRadians( 23.0 + 26.0/60.0 + 16.0/3600.0 );

    double declination = -maxDeclination * cos( osg::DegreesToRadians( 360.0 / 365.0 ) * ( dayNumber + 10.0 ) );
    double sinDeclination = sin( declination );
    double cosDeclination = cos( declination );

    double hourAngle = 0.2617993878 * ( localHour - 12.0 );
    double cosHourAngle = cos( hourAngle );

    double sinLatitude = sin( m_data->cameraData.latitude );
    double cosLatitude = cos( m_data->cameraData.latitude );

    double sinElevation = sinDeclination*sinLatitude
                        + cosDeclination*cosHourAngle*cosLatitude;

    if ( sinElevation >  1.0 ) sinElevation =  1.0;
    if ( sinElevation < -1.0 ) sinElevation = -1.0;

    m_elevation = asin( sinElevation );

    double cosElevation = cos( m_elevation );

    double cosAzimuth = ( sinDeclination*cosLatitude - cosHourAngle*cosDeclination*sinLatitude ) / cosElevation;

    if ( cosAzimuth >  1.0 ) cosAzimuth =  1.0;
    if ( cosAzimuth < -1.0 ) cosAzimuth = -1.0;

    if ( hourAngle < 0.0 )
    {
        m_azimuth =  acos( cosAzimuth );
    }
    else
    {
        m_azimuth = -acos( cosAzimuth );
    }

    //////////////////////////////////////////////
    m_skyDomeSunData->setElevation( m_elevation );
    //////////////////////////////////////////////

    m_data->sunData.elevation = m_elevation;
    m_data->sunData.azimuth   = m_azimuth;
}

////////////////////////////////////////////////////////////////////////////////

osg::ref_ptr<osg::Group> SkyDomeSun::createSkyDomeSunGroup()
{
    osg::ref_ptr<osg::Group> sunGroup = new osg::Group();
    sunGroup->setName( "SkyDomeSunGroup" );

    osg::ref_ptr<osg::PositionAttitudeTransform> sunPAT = new osg::PositionAttitudeTransform();
    sunPAT->setName( "SkyDomeSunPAT" );

    // sun face geode
    osg::ref_ptr<osg::Geode> sunGeode = new osg::Geode();
    sunGeode->setName( "SkyDomeSunGeode" );

    osg::ref_ptr<osg::Vec3Array> vertices = new osg::Vec3Array;
    osg::ref_ptr<osg::Vec2Array> texCoors = new osg::Vec2Array;
    osg::ref_ptr<osg::Vec3Array> normals  = new osg::Vec3Array;

    osg::ref_ptr<osg::Geometry> sunGeometry = new osg::Geometry();
    sunGeometry->setName( "SkyDomeSunGeometry" );

    double halfSize = 3.0 * SkyDomeSun::m_radius; // sun face diameter in 3 times smaller than texture size

    vertices->push_back( osg::Vec3d( -SkyDomeSun::m_distance, -halfSize, -halfSize ) );
    vertices->push_back( osg::Vec3d( -SkyDomeSun::m_distance,  halfSize, -halfSize ) );
    vertices->push_back( osg::Vec3d( -SkyDomeSun::m_distance,  halfSize,  halfSize ) );
    vertices->push_back( osg::Vec3d( -SkyDomeSun::m_distance, -halfSize,  halfSize ) );

    normals->push_back( osg::Vec3d( -1.0, 0.0, 0.0 ) );
    normals->push_back( osg::Vec3d( -1.0, 0.0, 0.0 ) );
    normals->push_back( osg::Vec3d( -1.0, 0.0, 0.0 ) );
    normals->push_back( osg::Vec3d( -1.0, 0.0, 0.0 ) );

    texCoors->push_back( osg::Vec2( 0, 1 ) );
    texCoors->push_back( osg::Vec2( 1, 1 ) );
    texCoors->push_back( osg::Vec2( 1, 0 ) );
    texCoors->push_back( osg::Vec2( 0, 0 ) );

    sunGeometry->setNormalArray( normals.get() );
    sunGeometry->setNormalBinding( osg::Geometry::BIND_PER_VERTEX );

    sunGeometry->setVertexArray( vertices.get() );

    sunGeometry->addPrimitiveSet( new osg::DrawArrays( osg::PrimitiveSet::QUADS, 0, vertices->size() ) );

    osg::ref_ptr<osg::StateSet> sunStateSet = sunGeode->getOrCreateStateSet();

    // texture
    std::string textureFilename = "data/textures/skydome/sun.png";
    osg::ref_ptr<osg::Image> sunImage = osgDB::readImageFile( textureFilename );

    if ( sunImage.valid() )
    {
        sunGeometry->setTexCoordArray( 0, texCoors.get() );

        osg::ref_ptr<osg::Texture2D> sunTexture = new osg::Texture2D();
        sunTexture->setImage( sunImage.get() );
        sunTexture->setWrap( osg::Texture2D::WRAP_T, osg::Texture2D::REPEAT );

        sunTexture->setMaxAnisotropy( 8.0f );

        sunStateSet->setMode( GL_LIGHTING , osg::StateAttribute::OFF );
        sunStateSet->setMode( GL_BLEND    , osg::StateAttribute::ON  );

        sunStateSet->setRenderingHint( osg::StateSet::TRANSPARENT_BIN );

        sunStateSet->setTextureAttributeAndModes( 0, sunTexture.get(), osg::StateAttribute::ON );
    }
    else
    {
        cerr << "ERROR! Cannot open texture file: " << textureFilename << endl;
    }

    osg::ref_ptr<osg::Material> sunMaterial = new osg::Material();

    sunMaterial->setColorMode( osg::Material::AMBIENT_AND_DIFFUSE );

    sunMaterial->setAmbient( osg::Material::FRONT, osg::Vec4( 1.0f, 1.0f, 1.0f, 1.0f ) );
    sunMaterial->setDiffuse( osg::Material::FRONT, osg::Vec4( 1.0f, 1.0f, 1.0f, 1.0f ) );

    sunStateSet->setAttribute( sunMaterial.get() );

    sunGeode->addDrawable( sunGeometry.get() );

    sunPAT->addChild( sunGeode.get() );

    sunGroup->addChild( sunPAT.get() );

    return sunGroup;
}
